Skip to content

fix(apptests): detect locked desktop / disconnected session#164

Merged
codemonkeychris merged 2 commits into
mainfrom
fix/apptests-detect-locked-desktop
May 6, 2026
Merged

fix(apptests): detect locked desktop / disconnected session#164
codemonkeychris merged 2 commits into
mainfrom
fix/apptests-detect-locked-desktop

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

Summary

  • E2E runs against WinAppDriver fail every Click() when the workstation locks (idle timeout, manual lock, RDP disconnect), surfacing as a flood of WebDriverException "An unknown error occurred in the remote end" that's indistinguishable from real test flake. Observed in a recent 50-run loop where 5 consecutive iterations failed 48/57 tests with identical stack traces, then recovered the moment the desktop was unlocked.
  • Add SessionInteractivityGuard (P/Invoke to OpenInputDesktop + WTSConnectState). Locked screens switch the input desktop from Default to Winlogon; RDP disconnect changes WTSConnectState to WTSDisconnected. Both are deterministic and cheap.
  • On detection: emit Assert.Inconclusive (not Fail) so the .trx outcome separates environmental from real failures, and write a marker file at $E2E_LOCK_MARKER_PATH so external multi-run loops can stop scheduling further iterations instead of generating more false negatives.

Hook points:

  • [TestInitialize] in AppTestBase — preflights every test method.
  • try/catch (WebDriverException) in NavigateToFixture — rechecks; lock → Inconclusive, otherwise rethrow as a real failure.
  • Same recheck around TestSession session bootstrap so a class-init lock bails out cleanly without spending 30s booting WinAppDriver into a doomed run.

Test plan

  • Build: dotnet build tests/Reactor.AppTests
  • Smoke run on unlocked desktop: dotnet test tests/Reactor.AppTests → 57/57 pass, no marker written.
  • Lock-while-running: run dotnet test, lock the workstation mid-run, verify subsequent tests are reported Inconclusive (not Failed) and the marker file is written.
  • Pre-locked: lock the workstation, run dotnet test, verify TestSession.AssemblyInit short-circuits before launching WinAppDriver/Host.

🤖 Generated with Claude Code

E2E runs against WinAppDriver fail every Click() when the workstation
locks (idle timeout, manual lock, RDP disconnect) — surfacing as a flood
of WebDriverException "An unknown error occurred in the remote end"
that's indistinguishable from real test flake. Observed in repeated runs
where 5+ consecutive iterations failed 48/57 tests with identical stack
traces, then recovered on next iteration once the desktop was unlocked.

Add SessionInteractivityGuard to query OpenInputDesktop (locked screen
switches the input desktop from "Default" to "Winlogon") and
WTSConnectState (RDP/console connect state). On detection:

- Emit Assert.Inconclusive instead of letting the test Fail, so the .trx
  outcome distinguishes environmental from real failures.
- Write a marker file at \$E2E_LOCK_MARKER_PATH so external runners can
  abort the rest of a multi-iteration loop instead of generating more
  false-negative results.

Hooks: [TestInitialize] in AppTestBase preflights every test, plus a
WebDriverException recheck in NavigateToFixture and TestSession session
bootstrap to catch locks that happen mid-operation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the reliability and diagnosability of tests/Reactor.AppTests runs by detecting non-interactive desktop conditions (locked workstation / disconnected session) and converting resulting WinAppDriver failures into MSTest Inconclusive, with an optional marker file to let external runners halt further iterations.

Changes:

  • Add SessionInteractivityGuard to detect locked/disconnected desktop state (OpenInputDesktop + WTS connect state), write a marker file, and throw Assert.Inconclusive.
  • Preflight interactivity before each test via [TestInitialize] in AppTestBase.
  • Recheck interactivity on WebDriverException during navigation (NavigateToFixture) and during TestSession bootstrap.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/Reactor.AppTests/Infrastructure/TestSession.cs Adds interactivity preflight and lock-aware recheck around WinAppDriver session bootstrap.
tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs New guard for detecting locked/disconnected sessions, emitting Inconclusive, and writing a marker file.
tests/Reactor.AppTests/Infrastructure/AppTestBase.cs Adds per-test interactivity preflight and recheck on WebDriver failures during fixture navigation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/Reactor.AppTests/Infrastructure/AppTestBase.cs Outdated
Comment thread tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs
Comment thread tests/Reactor.AppTests/Infrastructure/SessionInteractivityGuard.cs Outdated
Comment thread tests/Reactor.AppTests/Infrastructure/TestSession.cs
- AppTestBase: drop dead "final attempt" line + misleading comment.
  The for-loop's `when (attempt == 0)` filter meant iteration 1's
  WebDriverTimeoutException always propagated out, making the post-loop
  WaitForText unreachable.
- SessionInteractivityGuard: only treat OpenInputDesktop returning NULL
  as Locked when GetLastWin32Error is ERROR_ACCESS_DENIED. Other failure
  codes are Unknown and don't trigger Inconclusive — avoids masking real
  test failures behind transient Win32 errors.
- SessionInteractivityGuard: WriteMarker uses FileMode.CreateNew for an
  atomic first-writer-wins, replacing the racy File.Exists+WriteAllText
  pattern. Stale markers still won't get overwritten silently.
- TestSession: extend the bootstrap try/catch to cover WaitForHostWindow
  too, and handle TimeoutException as well as WebDriverException — a
  mid-init lock surfaces as a TimeoutException from the polling loop,
  not as a WebDriverException.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

Addressed all four review comments in bf0c012:

  1. AppTestBase L67-68 (dead code / misleading comment): the when (attempt == 0) filter meant iteration 1's WebDriverTimeoutException always propagated out, making the post-loop WaitForText line genuinely unreachable. Removed the dead lines and the comment rather than papering over it with a longer timeout — the existing in-loop retry has been working.

  2. SessionInteractivityGuard L33 (NULL handle ≠ Locked): now reads Marshal.GetLastWin32Error() immediately and only maps ERROR_ACCESS_DENIED (the documented signal that the calling thread can't access the active input desktop while Winlogon's secure desktop is up) to Locked. Other errors return Unknown, and EnsureInteractive/RecheckAfterWebDriverFailure now both pass Unknown through without throwing Inconclusive — avoids masking real failures.

  3. SessionInteractivityGuard L116 (race + stale carryover): swapped to FileMode.CreateNew for an atomic first-writer-wins. The IOException branch is the "marker already exists, preserve the originating writer" path. The runner is responsible for clearing the path between iterations (it points at a fresh per-run directory each loop, so stale carryover isn't a concern in the supported flow).

  4. TestSession L48 (mid-init lock surfaces as TimeoutException, not WebDriverException): extended the bootstrap try to also cover WaitForHostWindow(), and the catch now matches both WebDriverException and TimeoutException via a when pattern. A lock between the AssemblyInit preflight and the host window appearing is now reclassified to Inconclusive.

Build clean.

@codemonkeychris codemonkeychris merged commit 6d9fd00 into main May 6, 2026
6 checks passed
@codemonkeychris codemonkeychris deleted the fix/apptests-detect-locked-desktop branch May 6, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants